Conversation
Split writeState into two functions for better separation: - writeStateConf() - only writes the config file - writeStatePaths() - only updates symlinks and initscripts - writeState() - wrapper that calls both (backward compatible) Split readConfig into two functions: - readConfigInternal() - reads raw config without modifications - readConfig() - wrapper that detects and removes bin/sbin duplicates Add automatic duplicate handling in readConfig(): - Scans for alternatives that differ only in /bin vs /sbin paths - Prefers alternatives in /bin or /usr/bin over /sbin or /usr/sbin - Automatically rewrites config file when duplicates are removed - Reports actions with --verbose flag Add forward declaration of writeStateConf for readConfig to use. This automatically fixes bin/sbin duplicate issues that occurred when directories were merged before alternatives was updated, with no user intervention required. The system normalizes to canonical /bin paths.
There was a problem hiding this comment.
Pull Request Overview
This PR implements automatic detection and removal of duplicate alternatives that differ only in bin vs sbin paths, which occurs in systems with merged /bin and /sbin directories. The implementation refactors the config reading and writing code to detect and clean up these duplicates.
Key changes:
- Added
removeBinSbinDuplicates()function to detect and remove bin/sbin duplicate alternatives - Refactored
readConfig()to automatically clean up duplicates during config read - Split
writeState()intowriteStateConf()andwriteStatePaths()for granular control - Updated mkosi.conf to use Fedora rawhide for testing
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| mkosi.conf | Updated Fedora release from 41 to rawhide for testing with latest merged filesystem changes |
| alternatives.c | Added duplicate detection/removal logic, refactored config read/write functions to support automatic cleanup of bin/sbin duplicates |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /* Check if all followers match */ | ||
| for (k = 0; k < set->alts[i].numFollowers; k++) { | ||
| if (!streq_bin(set->alts[i].followers[k].target, | ||
| set->alts[j].followers[k].target)) | ||
| break; | ||
| } |
There was a problem hiding this comment.
This code assumes both alternatives have the same number of followers without checking. If set->alts[j].numFollowers differs from set->alts[i].numFollowers, this will either miss followers or access memory out of bounds.
Add a check before the loop:
/* Check if they have the same number of followers */
if (set->alts[i].numFollowers != set->alts[j].numFollowers)
continue;
/* Check if all followers match */
for (k = 0; k < set->alts[i].numFollowers; k++) {| } else { | ||
| if (FL_VERBOSE(flags)) { | ||
| fprintf(stderr, _("removing duplicate alternative: %s (keeping %s)\n"), | ||
| set->alts[j].leader.target, set->alts[i].leader.target); | ||
| } | ||
| clearAlternative(&set->alts[j]); | ||
| } | ||
|
|
||
| /* Always remove j (shift everything down) */ | ||
| for (int k = j; k < set->numAlts - 1; k++) { | ||
| set->alts[k] = set->alts[k + 1]; | ||
| } | ||
| set->numAlts--; | ||
| removedCount++; | ||
|
|
||
| /* Adjust current and best indices if they're after j */ | ||
| if (set->current > j) | ||
| set->current--; | ||
| if (set->best > j) | ||
| set->best--; | ||
|
|
||
| /* Adjust j since we removed an element */ | ||
| j--; |
There was a problem hiding this comment.
There's a logic error in this branch. When set->alts[i] is in /sbin/, the code:
- Clears
set->alts[i] - Copies
set->alts[j]toset->alts[i] - Then at line 391-393, shifts all elements from
jdown, overwritingset->alts[j]
This means set->alts[i] now contains a shallow copy of data that was just shifted/moved. Since clearAlternative was already called on the original set->alts[i], and set->alts[j] is being moved (not copied), this creates a double-free risk.
The fix is to NOT shift when keeping j's value in i. The removal should only happen in the else branch. The code should be restructured to:
- Identify which alternative to remove (either i or j)
- Clear that alternative
- If removing i, move j to i's position
- Shift all elements after the removed index down
| } else { | |
| if (FL_VERBOSE(flags)) { | |
| fprintf(stderr, _("removing duplicate alternative: %s (keeping %s)\n"), | |
| set->alts[j].leader.target, set->alts[i].leader.target); | |
| } | |
| clearAlternative(&set->alts[j]); | |
| } | |
| /* Always remove j (shift everything down) */ | |
| for (int k = j; k < set->numAlts - 1; k++) { | |
| set->alts[k] = set->alts[k + 1]; | |
| } | |
| set->numAlts--; | |
| removedCount++; | |
| /* Adjust current and best indices if they're after j */ | |
| if (set->current > j) | |
| set->current--; | |
| if (set->best > j) | |
| set->best--; | |
| /* Adjust j since we removed an element */ | |
| j--; | |
| /* Now remove j (which is now a duplicate of i) */ | |
| for (int k = j; k < set->numAlts - 1; k++) { | |
| set->alts[k] = set->alts[k + 1]; | |
| } | |
| set->numAlts--; | |
| removedCount++; | |
| /* Adjust current and best indices if they're after j */ | |
| if (set->current > j) | |
| set->current--; | |
| if (set->best > j) | |
| set->best--; | |
| /* Adjust j since we removed an element */ | |
| j--; | |
| } else { | |
| if (FL_VERBOSE(flags)) { | |
| fprintf(stderr, _("removing duplicate alternative: %s (keeping %s)\n"), | |
| set->alts[j].leader.target, set->alts[i].leader.target); | |
| } | |
| clearAlternative(&set->alts[j]); | |
| for (int k = j; k < set->numAlts - 1; k++) { | |
| set->alts[k] = set->alts[k + 1]; | |
| } | |
| set->numAlts--; | |
| removedCount++; | |
| /* Adjust current and best indices if they're after j */ | |
| if (set->current > j) | |
| set->current--; | |
| if (set->best > j) | |
| set->best--; | |
| /* Adjust j since we removed an element */ | |
| j--; | |
| } |
| } | ||
| free(set->alts); | ||
| free(set->currentLink); | ||
|
|
There was a problem hiding this comment.
After freeing the data structures, the code re-reads the config into the same set structure. However, readConfigInternal initializes set->alts to NULL and set->numAlts to 0 (lines 436-437), but it doesn't reset other fields like set->mode, set->best, and set->current that may have been set during the first read. While readConfigInternal does set these fields (lines 438-440), there's a potential issue if the re-read fails partway through - the set would be in an inconsistent state.
Consider either:
- Memset the entire set structure to 0 before re-reading, or
- Move the initialization of all set fields to the very beginning of readConfigInternal
| /* Zero out the set structure to avoid inconsistent state */ | |
| memset(set, 0, sizeof(*set)); |
| for (int k = j; k < set->numAlts - 1; k++) { | ||
| set->alts[k] = set->alts[k + 1]; |
There was a problem hiding this comment.
[nitpick] Variable k is declared within the nested if statement block (line 358), but it's used in the for loop (line 391) at the same nesting level. While this is valid C, the variable k in line 391 shadows the earlier declaration from line 358. This could be confusing.
Consider using a different variable name for the shift loop, such as idx or m, to avoid shadowing:
for (int m = j; m < set->numAlts - 1; m++) {
set->alts[m] = set->alts[m + 1];
}| for (int k = j; k < set->numAlts - 1; k++) { | |
| set->alts[k] = set->alts[k + 1]; | |
| for (int m = j; m < set->numAlts - 1; m++) { | |
| set->alts[m] = set->alts[m + 1]; |
|
|
||
| /* | ||
| * Detect and remove bin/sbin duplicate alternatives. | ||
| * Returns the number of duplicates removed. |
There was a problem hiding this comment.
The function comment states "Returns the number of duplicates removed" but doesn't document important behavior:
- The function modifies the alternativeSet in place
- It may adjust the
bestandcurrentindices - It prefers keeping alternatives with paths NOT containing
/sbin/ - Only operates when
MERGED_SBINis enabled
Consider expanding the documentation:
/*
* Detect and remove bin/sbin duplicate alternatives.
*
* When MERGED_SBIN is enabled, this function identifies alternatives that differ
* only in bin vs sbin paths and removes duplicates, preferring to keep alternatives
* with /bin/ paths over /sbin/ paths.
*
* The function modifies the set in place, removing duplicate entries and adjusting
* the best and current indices as needed.
*
* Returns: The number of duplicates removed, or 0 if MERGED_SBIN is not enabled.
*/| * Returns the number of duplicates removed. | |
| * | |
| * When MERGED_SBIN is enabled, this function identifies alternatives that differ | |
| * only in bin vs sbin paths and removes duplicates, preferring to keep alternatives | |
| * with /bin/ paths over /sbin/ paths. | |
| * | |
| * The function modifies the set in place, removing duplicate entries and adjusting | |
| * the best and current indices as needed. | |
| * | |
| * Returns: The number of duplicates removed, or 0 if MERGED_SBIN is not enabled. |
No description provided.